{% extends "base.html" %}

{% block title %}设备远程控制{% end %}


{% block style %}
<style>
  html,
  body,
  #content-wrapper {
    height: 100%;
  }

  #content-wrapper {
    display: flex;
    flex-direction: column;
  }

  #app {
    display: flex;
    flex-grow: 1;
    min-height: 0%;
  }

  .height100 {
    height: 100%;
  }

  .height-auto-fill {
    height: -webkit-fill-available;
  }

  .flex-reset {
    min-height: 0%;
  }

  .grow-0 {
    flex-grow: 0;
  }

  .grow-1 {
    flex-grow: 1;
  }

  .nopadding {
    padding: 0px;
  }

  .cursor-pointer {
    cursor: pointer;
  }

  .color-wrong {
    color: red;
  }

  .debugarea {
    /* background-color: #ddd; */
    border: 1px solid red;
  }

  .screen {
    position: relative;
    background-color: gray;
  }

  .canvas-fg {
    z-index: 20;
    position: absolute;
  }

  .canvas-bg {
    z-index: 10;
  }

  .bottom-gutter {
    margin-bottom: 10px;
  }

  .finger {
    position: absolute;
    border-style: solid;
    border-radius: 50%;
    border-color: white;
    border-width: 1mm;
    width: 6mm;
    height: 6mm;
    top: -3mm;
    left: -3mm;
    opacity: 0.7;
    pointer-events: none;
    background: red;
    z-index: 200;
    display: none;
  }

  .finger-1 {
    background-color: blue;
  }

  .finger.active {
    display: block;
  }

  .finger.longClick {
    border: 1mm solid rebeccapurple;
  }

  .card-columns {
    column-count: 2;
  }
</style>
{% end %}

{% block nav %}
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark">
  <div class="container-fluid">
    <a class="navbar-brand" href="/">
      <span class="title">ATXServer2</span></a>
    </a>
    <div class="collapse navbar-collapse" id="navbarNavDropdown">
      <div class="navbar-nav">
        <a class="nav-item nav-link active" href="/">
          <i class="fab fa-apple"></i> 苹果设备</a>
        <!-- <a class="nav-item nav-link active" href='/'> -->
        <!-- </a> -->
      </div>
      <div class="navbar-nav navbar-right ml-auto">
        <div class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
            aria-haspopup="true" aria-expanded="false">
            {{current_user.email}}
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
            <a class="dropdown-item" href="/user">用户信息</a>
            <a class="dropdown-item" href="/logout">Logout</a>
          </div>
        </div>
        <form class="form-inline">
          <button class="btn btn-warning" type="button" @click="stopUsing">停止使用</button>
          <span ref="usingTime" class="navbar-text" style="margin-left: 1em">
            使用时长
          </span>
        </form>
      </div>
    </div>

    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown"
      aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
  </div>
</nav>
{% end %}


{% block content %}
<div class="container-fluid d-flex flex-column">
  <div class="row grow-1 flex-reset">
    <div class="col-sm debugarea d-flex flex-column justify-content-center nopadding grow-0 height-auto-fill">
      <section class="debugarea" style="height: 2rem; line-height: 2rem; padding: 0 10px; justify-self: start">
        <span>
          <i class="fas fa-mobile-alt cursor-pointer" :class='{"fa-rotate-90": landscape}'></i>
        </span>
        <span>
          <i v-if="displayLinked" class="fas fa-link" style="color: green"></i>
          <i v-else @click="mirrorDisplay" class="fas fa-unlink" style="color: red"></i>
        </span>
      </section>
      <!-- screen body -->
      <section class="screen debugarea d-flex grow-1 align-items-center justify-content-center flex-reset"
        style="flex-basis: 0%; line-height: 0px">
        <!-- <canvas ref="fgCanvas" class="canvas-fg" v-bind:style="canvasStyle"></canvas> -->
        <canvas ref="bgCanvas" class="canvas-bg" v-bind:style="canvasStyle"></canvas>
        <span class="finger finger-0" style="transform: translate3d(200px, 100px, 0px)"></span>
        <span class="finger finger-1" style="transform: translate3d(200px, 100px, 0px)"></span>
        <!-- <img style="z-index: 10" v-if="loading" src="/assets/loading.svg"> -->
      </section>
      <section class="footer d-flex justify-content-around debugarea">
        <button class="btn btn-default grow-1" @click="pressHome">
          <i class="fas fa-home"></i>
        </button>
      </section>
    </div>
    <div class="col-sm debugarea height-auto-fill flex-reset" style="overflow-y: auto">
      <el-tabs v-model="activeName" @tab-click="handleTabClick">
        <el-tab-pane label="常用" name="common">
          <div class="card-columns">
            <div class="card">
              <div class="card-header">
                <i class="fab fa-angellist"></i> 常用功能
                <span style="font-size: 0.7em; padding-top: 5px; color: gray">注: iOS的弹窗不能通过屏幕点击来选择</span>
              </div>
              <div class="card-body">
                <el-button size="mini" @click="hotfix">
                  <i class="fas fa-hammer"></i>
                  修复旋转
                </el-button>
                <el-button size="mini" @click="screenshot">截图</el-button>

                <el-button size="mini" :loading="alert.loading" @click="chooseAlertButtons">
                  选择弹框按钮
                </el-button>
                <el-dialog title="弹窗选择" :visible.sync="alert.visible">
                  <span style="padding: 5px" v-for="v in alert.buttons">
                    <el-button round size="small" @click="alertAccept(v); alert.visible=false">{{!v}}</el-button>
                  </span>
                </el-dialog>

              </div>
            </div>

            <div class="card">
              <div class="card-header">
                <i class="fas fa-bolt"></i> 快捷启动
              </div>
              <div class="card-body">
                <el-button round size="small" @click="appOpen('com.apple.Preferences')">
                  <i class="el-icon-setting"></i>
                  设置
                </el-button>
              </div>
            </div>

            <div class="card">
              <div class="card-header">
                <i class="fas fa-bug"></i> 调试信息
              </div>
              <div class="card-body">
                <dl>
                  <dt>Display</dt>
                  <dd><code>{{!display.width}}x{{!display.height}}</code></dd>
                  <dt>SessionID</dt>
                  <dd><code v-text="session.id"></code></dd>
                  <dt>FPS/Frames</dt>
                  <dd>
                    <code v-text="display.fps"></code>
                    /
                    <small><code v-text="session.frameCount"></code></small>
                  </dd>
                  <dt>WDA URL</dt>
                  <dd>
                    <code v-text="source.wdaUrl"></code>
                    <i :data-clipboard-text="source.wdaUrl" class="far fa-copy clipboard-copy cursor-pointer"></i>
                  </dd>
                </dl>
              </div>
            </div>

            <div class="card">
              <div class="card-header">
                <i class="fas fa-cubes"></i> 安装管理
              </div>
              <div class=card-body">
                <!-- <el-button>Accept(TODO)</el-button> -->
                <div style="padding: 10px">
                  <el-upload ref="upload" drag accept=".ipa" action="/uploads" :on-success="onUpload"
                    :on-preview="onUploadSelect">
                    <i class="el-icon-upload"></i>
                    <div class="el-upload__text">将安装包拖到此处安装，或<em>点击上传</em></div>
                    <div class="el-upload__tip" slot="tip">只能上传ipa文件，且不超过2G</div>
                  </el-upload>
                  <div class="form-group">
                    <label>URL</label>
                    <input type="text" placeholder="http://..." v-model="app.url" class="form-control form-control-sm">
                  </div>
                  <el-button @click="appInstall" :loading="!app.finished" :disabled="!app.finished || !app.url"
                    size="small">安装</el-button>
                  <hr v-if="!!app.message">
                  <pre style="white-space: normal" v-text="app.message"></pre>
                </div>
              </div>
            </div>

          </div>
        </el-tab-pane>
      </el-tabs>
    </div>
  </div>
</div>
</div>
{% end %}

{% block script %}
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.4/dist/clipboard.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/css-element-queries@1.1.1/src/ResizeSensor.min.js"></script>
<script src="{{static_url('javascripts/imagepool.js')}}"></script>
<script>
  const udid = "{{udid}}"
  const userEmail = "{{current_user.email}}"


  $.getJSON("/api/v1/user/devices/" + udid)
    .then(ret => {
      vm = new Vue({
        el: "#content-wrapper",
        data: Object.assign({
          activeName: 'common',
          canvas: {
            bg: null,
            fg: null,
          },
          canvasStyle: {
            opacity: 1,
            width: '400px',
            height: 'unset',
            maxHeight: "unset",
          },
          rotation: 0,
          session: {
            id: '',
            frameCount: 0,
          },
          display: {
            width: 0,
            height: 0,
            fps: 0,
            ws: null,
          },
          alert: {
            buttons: [],
            loading: false,
            visible: false,
          },
          app: {
            message: "",
            finished: true,
          },
          pageHidden: false,
          imagePool: new ImagePool(100),
        }, ret.device),
        computed: {
          landscape() {
            return this.display.width > this.display.height;
          },
          displayLinked() {
            return this.display.ws !== null
          },
          displayWSUrl() {
            let url = this.source.wdaUrl.replace(/^https?:\/\//, "ws://") + "/screen"
            return url
          }
        },
        methods: {
          screenshot() {
            $.ajax({
              url: this.path2url("/screenshot"),
              dataType: "json",
            }).then(ret => {
              if (window.navigator.msSaveOrOpenBlob) {
                alert("IE is not supported !!!")
                return
              }
              var a = document.createElement("a");
              a.href = "data:image/jpeg;base64," + ret.value;
              a.download = "screen-" + new Date().getTime() + ".jpg";
              a.click();
              setTimeout(function () {
                document.body.removeChild(a);
              }, 0);
            })
          },
          appOpen(bundleId) {
            return $.ajax({
              method: "post",
              url: this.path2url("/session"),
              data: JSON.stringify({
                capabilities: {
                  alwaysMatch: {
                    bundleId: bundleId,
                    shouldWaitForQuiescence: true,
                  }
                }
              })
            }).then(ret => {
              this.session.id = ret.sessionId
            })
          },
          chooseAlertButtons() {
            this.alert.buttons = []
            this.alert.loading = true
            return $.ajax({
              method: "get",
              url: this.path2url("/session/" + this.session.id + "/wda/alert/buttons")
            }).then(ret => {
              this.alert.buttons = ret.value
              this.$nextTick(() => {
                this.alert.visible = true
              })
              // this.$alert("未检测到系统弹窗")
              // this.$alert("未知异常，打开开发者选项查看问题
            }).always(() => {
              this.alert.loading = false
            })
          },
          alertAccept(name) {
            let data = null;
            if (typeof name === 'string' || name instanceof String) {
              data = JSON.stringify({ name: name })
            }
            return $.ajax({
              method: "post",
              url: this.path2url("/session/" + this.session.id + "/alert/accept"),
              data: data,
            })
          },
          initClipboardJS() {
            var clipboard = new ClipboardJS(".clipboard-copy")

            clipboard.on('success', function (e) {
              e.clearSelection()
              showTooltip(e.trigger, "Copied!")
            })

            document.querySelectorAll(".clipboard-copy").forEach(e => {
              e.addEventListener('mouseleave', clearTooltip);
              e.addEventListener('blur', clearTooltip);
            })

            function clearTooltip(e) {
              const target = e.currentTarget;
              setTimeout(() => {
                target.innerHTML = ""
              }, 200)
            }

            function showTooltip(elem, msg) {
              elem.innerHTML = "&nbsp;<small>" + msg + "</small>"
            }
          },
          onUploadSelect(file) {
            this.app.url = file.response.data.url;
          },
          onUpload(resp, file, files) {
            if (!resp.success) {
              this.$message({
                message: resp.description,
                type: "error",
              })
              return
            }
            this.app.url = resp.data.url;
            return this.appInstall()
          },
          appInstall() {
            const url = this.app.url

            this.app.finished = false
            this.app.message = "安装中 ..."
            $.ajax({
              method: "post",
              url: this.source.url + "/app/install?udid=" + this.udid,
              data: {
                url: url,
              }
            }).done(ret => {
              this.app.message = "安装成功"
            }).fail(err => {
              this.app.message = "安装失败 " + err.responseJSON.description;
            }).always(() => {
              this.app.finished = true
            })
          },
          showFPS() {
            let frame = this.session.frameCount;
            setInterval(() => {
              this.display.fps = this.session.frameCount - frame
              frame = this.session.frameCount;
            }, 1000);
          },
          closeWindowWhenReleased(interval) {
            setTimeout(() => {
              if (document.hidden) {
                $.getJSON("/api/v1/user/devices/" + udid)
                  .done(ret => {
                    this.closeWindowWhenReleased(5000)
                  })
                  .fail(ret => {
                    console.log(ret)
                    if (ret.status === 403) { // forbidden
                      window.close()
                      return
                    }
                  })
              } else {
                $.getJSON("/api/v1/user/devices/" + udid + "/active")
                  .done((ret) => {
                    this.closeWindowWhenReleased(interval)
                  })
                  .fail(function (ret) {
                    console.log(ret)
                    alert("设备可能被释放了，Press F12 to debug")
                    // let content = '设备' + this.idleTimeout + "秒内没有操作，设备自动释放，点击刷新重新占用该设备"
                    // this.$alert(content, '设备超时提示', {
                    //   confirmButtonText: '刷新',
                    //   type: 'warning'
                    // }).then(() => {
                    //   location.reload()
                    // }).catch(() => {
                    //   window.close()
                    // })
                  })
              }
            }, interval)
          },
          pressHome() {
            return $.ajax({
              url: this.path2url("/wda/homescreen"),
              method: "POST",
            }).then(() => {
              setTimeout(() => {
                this.hotfix()
              }, 500)
            })
          },
          getSessionId() {
            return $.ajax({
              method: "post",
              url: this.path2url("/session"),
              data: '{"capabilities": {}}',
            }).then(ret => {
              return ret.sessionId;
            })
          },
          hotfix() {
            return $.ajax({
              url: this.path2url("/status")
            }).then(ret => {
              if (ret.sessionId) {
                return ret.sessionId
              }
              return this.getSessionId()
            }).then((sessionId) => {
              this.session.id = sessionId;
              return $.ajax({
                url: this.path2url("/session/" + this.session.id + "/window/size")
              })
            }).then(ret => {
              if (ret.value.width && ret.value.height) { // width and height might be 0
                this.display.width = ret.value.width;
                this.display.height = ret.value.height;
              }
            })
          },
          stopUsing() {
            $.ajax({
              method: "delete",
              url: "/api/v1/user/devices/" + this.udid,
              dataType: "json"
            }).always(() => {
              window.close()
            })
          },
          handleTabClick(tab, event) {
            console.log(tab.name)
          },
          path2url(pathname) {
            return this.source.wdaUrl + pathname
          },
          disableTouch() {
            let element = this.canvas.bg;
            element.style.cursor = 'not-allowed' // set el.style is not working
            element.style.pointerEvents = "none"
          },
          enableTouch() {
            let element = this.canvas.bg;
            element.style.cursor = ''
            element.style.pointerEvents = ""
          },
          mirrorDisplay() {
            let ws = new WebSocket(this.displayWSUrl)
            this.display.ws = ws;

            ws.onopen = () => {
              console.log("screen connected")
              this.canvasStyle.opacity = 1
              this.enableTouch()
            }
            ws.onmessage = (message) => {
              if (message.data instanceof Blob) {
                this.session.frameCount += 1
                this.drawBlobImageToCanvas(message.data, this.canvas.bg, this.landscape)
              }
            }
            ws.onclose = () => {
              if (this.display.ws === ws) {
                this.display.ws = null;
                this.$message({
                  showClose: true,
                  message: "设备屏幕同步中断",
                  type: "error",
                })
                this.canvasStyle.opacity = 0.5
                this.disableTouch()
              }
            }
          },
          closeMirrorDisplay() {
            this.canvasStyle.opacity = 0.5
            if (this.display.ws) {
              let ws = this.display.ws;
              this.display.ws = null;
              ws.close()
            }
          },
          syncTouchpad() {
            let bounds = {}
            let element = this.canvas.bg;

            function calculateBounds() {
              var el = element;
              bounds.w = el.offsetWidth
              bounds.h = el.offsetHeight
              bounds.x = 0
              bounds.y = 0

              while (el.offsetParent) {
                bounds.x += el.offsetLeft
                bounds.y += el.offsetTop
                el = el.offsetParent
              }
            }

            let coords = (e) => {
              let x = e.pageX - bounds.x
              let y = e.pageY - bounds.y
              x = Math.max(0, Math.min(bounds.w, x))
              y = Math.max(0, Math.min(bounds.h, y))

              return {
                fingerX: x + element.offsetLeft,
                fingerY: y + element.offsetTop,
                x: Math.floor(x / bounds.w * this.display.width),
                y: Math.floor(y / bounds.h * this.display.height)
              }
            }


            let mousePos = {
              beganAt: null,
              down: null,
              up: null,
            }

            let wdaTouch = (x, y) => {
              return $.ajax({
                method: "POST",
                url: this.path2url("/session/" + this.session.id + "/wda/tap/0"),
                data: JSON.stringify({ x, y })
              })
            }

            let wdaSwipe = (fromX, fromY, toX, toY, duration) => {
              return $.ajax({
                url: this.path2url("/session/" + this.session.id + "/wda/dragfromtoforduration"),
                method: "POST",
                data: JSON.stringify({
                  fromX: fromX,
                  fromY: fromY,
                  toX: toX,
                  toY: toY,
                  duration: duration || 0.01, // seconds
                })
              })
            }

            let mouseUpOperate = (x, y) => {
              let duration = new Date() - mousePos.beganAt; // milliseconds
              console.log("hold duration", duration);

              const
                startX = mousePos.down.x,
                startY = mousePos.down.y,
                moveX = Math.abs(startX - x),
                moveY = Math.abs(startY - y);

              if (moveX == 0 && moveY == 0) {
                if (duration < 200) { // click
                  // click
                  console.log("touch", x, y)
                  return wdaTouch(x, y)
                } else {
                  // long click
                  console.log("touchHold", x, y)
                  return wdaSwipe(startX, startY, x, y, duration / 1000)
                }
              } else {
                console.log("swipe:", mousePos.down, "to", { x, y }, duration)
                return wdaSwipe(startX, startY, x, y, 0.01)
              }
            }

            const mouseDownListener = (event) => {
              let e = event;
              if (e.originalEvent) {
                e = e.originalEvent
              }
              e.preventDefault()

              // Middle click equals HOME
              if (e.which === 2) {
                this.pressHome()
                return
              }

              fakePinch = e.altKey
              calculateBounds()

              let { fingerX, fingerY, x, y } = coords(e)

              activeFinger(0, fingerX, fingerY);
              mousePos.beganAt = new Date()
              mousePos.down = { x, y }

              document.addEventListener('mousemove', mouseMoveListener);
              document.addEventListener('mouseup', mouseUpListener);
            }

            function mouseMoveListener(event) {
              var e = event
              if (e.originalEvent) {
                e = e.originalEvent
              }
              e.preventDefault()

              let { fingerX, fingerY } = coords(e)
              var pressure = 0.5
              activeFinger(1, fingerX, fingerY, pressure);
            }

            function mouseUpListener(event) {
              var e = event
              if (e.originalEvent) {
                e = e.originalEvent
              }
              e.preventDefault()

              element.style.cursor = "not-allowed" // not working
              element.style.pointerEvents = "none"

              let { fingerX, fingerY, x, y } = coords(e)

              activeFinger(1, fingerX, fingerY);

              stopMousing()
              element.removeEventListener("mousedown", mouseDownListener)

              mouseUpOperate(x, y).then(() => {
                recoverFingersAndMouse()
              })
            }

            function stopMousing() {
              document.removeEventListener('mousemove', mouseMoveListener);
              document.removeEventListener('mouseup', mouseUpListener);
            }

            function activeFinger(index, x, y, pressure) {
              var scale = 0.5 + (pressure || 0.5)
              $(".finger-" + index)
                .addClass("active")
                .css("transform", 'translate3d(' + x + 'px,' + y + 'px,0)')
            }

            function deactiveFinger(index) {
              $(".finger-" + index).removeClass("active")
            }

            function recoverFingersAndMouse() {
              deactiveFinger(0)
              deactiveFinger(1)
              element.style.cursor = ""
              element.style.pointerEvents = ""
              element.addEventListener('mousedown', mouseDownListener);
            }

            function preventHandler(event) {
              event.preventDefault()
            }

            /* bind listeners */
            element.addEventListener('mousedown', mouseDownListener);
          },
          fitCanvas(canvas) {
            if (canvas.width > canvas.height) {
              // 横屏显示，宽高相等
              this.canvasStyle.maxHeight = canvas.parentElement.clientHeight + "px";
              this.canvasStyle.height = "auto"
              this.canvasStyle.width = canvas.parentElement.clientHeight + "px"
            } else {
              this.canvasStyle.maxHeight = "unset"
              this.canvasStyle.height = canvas.parentElement.clientHeight + "px"
              this.canvasStyle.width = "auto"
            }
          },
          drawBlobImageToCanvas(blob, canvas, landscape) {
            // Support jQuery Promise
            var dtd = $.Deferred();
            var ctx = canvas.getContext('2d'),
              URL = window.URL || window.webkitURL,
              BLANK_IMG =
                '',
              img = this.imagePool.next();

            img.onload = () => {
              canvas.width = img.width
              canvas.height = img.height

              ctx.drawImage(img, 0, 0, img.width, img.height);

              if (landscape) {
                // 顺时针旋转 270°
                canvas.width = img.height
                canvas.height = img.width

                ctx.save()
                ctx.translate(canvas.width / 2, canvas.height / 2);
                ctx.rotate(1.5 * Math.PI);
                ctx.drawImage(img,
                  -img.width / 2,
                  -img.height / 2);
                ctx.restore();
              }
              this.fitCanvas(canvas)

              // Try to forcefully clean everything to get rid of memory
              // leaks. Note self despite this effort, Chrome will still
              // leak huge amounts of memory when the developer tools are
              // open, probably to save the resources for inspection. When
              // the developer tools are closed no memory is leaked.
              img.onload = img.onerror = null
              img.src = BLANK_IMG
              img = null
              blob = null

              URL.revokeObjectURL(url)
              url = null
              dtd.resolve();
            }

            img.onerror = function () {
              // Happily ignore. I suppose this shouldn't happen, but
              // sometimes it does, presumably when we're loading images
              // too quickly.

              // Do the same cleanup here as in onload.
              img.onload = img.onerror = null
              img.src = BLANK_IMG
              img = null
              blob = null

              URL.revokeObjectURL(url)
              url = null
              dtd.reject();
            }

            var url = URL.createObjectURL(blob)
            img.src = url;
            return dtd;
          },
        },
        mounted: function () {
          this.canvas.bg = this.$refs.bgCanvas;

          // save the bandwidth
          document.addEventListener("visibilitychange", () => {
            this.pageHidden = document.hidden;
            if (this.pageHidden) {
              this.closeMirrorDisplay()
            } else {
              this.mirrorDisplay()
            }
          })

          this.mirrorDisplay()
          this.syncTouchpad()
          this.hotfix()

          // show fps
          this.showFPS()

          // 防止设备被自动释放
          this.closeWindowWhenReleased(10000)

          // 更新使用时间
          setInterval(() => {
            let duration = moment.now() - moment(this.usingBeganAt)
            this.$refs.usingTime.innerHTML = moment.utc(duration).format("HH:mm:ss")
          }, 1000)

          // 初始化剪贴板
          this.initClipboardJS()
        },
      })
    })
</script>
{% end %}